#pragma once

#include <algorithm>
#include <limits>

#include "base.h"
#include "../impl/ma.h"
#include "../updater/minmax.h"
#include "../container/robuf.h"

namespace mdtx::online::indicator
{
    /**
     * @brief Rolling minmax
     *
     * @tparam T scala type
     */
    template <typename T>
    using minmax = detail::indi_wrap_ro<updater::minmax<T>>;

    /**
     * @brief Rolling highlow
     *
     * @tparam T scala type
     */
    template <typename T>
    using highlow = detail::indi_wrap_ro<updater::highlow<T>>;

    /**
     * @brief Rolling min
     *
     * @tparam T scala type
     */
    template <typename T>
    using min = detail::indi_wrap_ro<updater::min<T>>;

    /**
     * @brief Rolling max
     *
     * @tparam T scala type
     */
    template <typename T>
    using max = detail::indi_wrap_ro<updater::max<T>>;

    /**
     * @brief Rolling argmin/argmax
     *
     * @tparam T scala type
     */
    template <typename T>
    using argminmax = detail::indi_wrap_ro<updater::argminmax<T>>;

    /**
     * @brief Rolling argmin
     *
     * @tparam T scala type
     */
    template <typename T>
    using argmin = detail::indi_wrap_ro<updater::argmin<T>>;

    /**
     * @brief Rolling argmax
     *
     * @tparam T scala type
     */
    template <typename T>
    using argmax = detail::indi_wrap_ro<updater::argmax<T>>;

    /**
     * @brief Cumulative min
     *
     * @tparam T scala type
     */
    template <typename T>
    class min_cu : indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        T min{std::numeric_limits<T>::max()};

    public:
        tuple_out update(tuple_in const &in) override
        {
            min = std::min(std::get<0>(in), min);
            return tuple_out(min);
        }
    };

    /**
     * @brief Cumulative max
     *
     * @tparam T scala type
     */
    template <typename T>
    class max_cu : indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        T max{std::numeric_limits<T>::lowest()};

    public:
        tuple_out update(tuple_in const &in) override
        {
            max = std::max(std::get<0>(in), max);
            return tuple_out(max);
        }
    };

    /**
     * @brief Rolling rank
     *
     * @tparam T scala type
     */
    template <typename T>
    class rank : indi_base<T, ptrdiff_t, 1, 1>
    {
        using base = indi_base<T, ptrdiff_t, 1, 1>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        container::sdeque<T> sorted{};

    public:
        explicit rank(ptrdiff_t period)
            : buf(period)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in);
            if (buf.full())
            {
                sorted.erase(buf.back());
            }
            auto it = sorted.insert(x);
            buf.push_back(x);

            return tuple_out(std::distance(sorted.begin(), it));
        }
    };

    /**
     * @brief Rolling median
     *
     * @tparam T scala type
     */
    template <typename T>
    class median : indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        container::sorted_robuf<T> buf;
        ptrdiff_t midx;
        bool odd;

    public:
        explicit median(ptrdiff_t period)
            : buf(period),
              midx(period / 2),
              odd(period % 2)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            buf.insert(std::get<0>(in));

            T med;
            if (buf.buf.full())
            {
                if (odd)
                {
                    med = buf.sorted[midx];
                }
                else
                {
                    med = (buf.sorted[midx - 1] + buf.sorted[midx]) / 2;
                }
            }
            else
            {
                auto tmp_odd = buf.buf.size() % 2;
                auto tmp_midx = buf.buf.size() / 2;
                if (tmp_odd)
                {
                    med = buf.sorted[tmp_midx];
                }
                else
                {
                    med = (buf.sorted[tmp_midx - 1] + buf.sorted[tmp_midx]) / 2;
                }
            }

            return tuple_out(med);
        }
    };

    /**
     * @brief Rolling weighted median
     *
     * @tparam T scala type
     */
    template <typename T>
    class median_vw : indi_weighted1<T>
    {
        using base = indi_weighted1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        container::wsorted_robuf<T, T> buf;
        impl::sum<T> wsum{};

    public:
        explicit median_vw(ptrdiff_t period)
            : buf(period)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            auto [x, w] = in;
            if (buf.buf.full())
            {
                const auto &old = buf.buf.front();
                wsum.roll_impl(w, old.w);
            }
            else
            {
                wsum.insert_impl(w);
            }
            buf.insert(x, w);

            T w_acc{}, w_half{wsum.s / 2}, med;
            for (auto [val, weight] : buf.sorted)
            {
                w_acc += weight;
                if (w_acc > w_half)
                {
                    med = val;
                    break;
                }
            }

            return tuple_out(med);
        }
    };

    /**
     * @brief Rolling Kth element
     *
     * @tparam T scala type
     */
    template <typename T>
    class kth : indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        container::sorted_robuf<T> buf;
        ptrdiff_t k;

    public:
        kth(ptrdiff_t period, ptrdiff_t k)
            : buf(period),
              k(k)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            buf.insert(std::get<0>(in));
            T val = buf.sorted.size() > k ? buf.sorted[k] : buf.sorted.back();
            return tuple_out(val);
        }
    };
}
